{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# <center>Прогноз просрочки кредита заемщиком</center>\n",
    "<center> Автор: Денис Сенькин"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import pandas as pd\n",
    "import seaborn as sns\n",
    "from tqdm import tqdm_notebook as tqdm\n",
    "\n",
    "%matplotlib inline\n",
    "from matplotlib import pyplot as plt\n",
    "plt.style.use(['seaborn-darkgrid'])\n",
    "plt.rcParams['font.family'] = 'DejaVu Sans'\n",
    "\n",
    "from sklearn import metrics\n",
    "from sklearn.model_selection import GridSearchCV\n",
    "from sklearn.preprocessing import StandardScaler\n",
    "from sklearn.model_selection import StratifiedKFold\n",
    "from sklearn.ensemble import RandomForestClassifier\n",
    "from sklearn.linear_model import LogisticRegression\n",
    "from sklearn.model_selection import validation_curve, learning_curve\n",
    "RANDOM_STATE = 17"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Описание набора данных\n",
    "\n",
    "Данные представляют собой исторические данные по 251503 заемщикам (https://www.kaggle.com/c/GiveMeSomeCredit). Задача является бинарной классификацией. Цель - предсказать будет ли тот или иной заемщик испытывать финансовые трудности в ближайшие 2 года, т.е. будет ли просрочка по займу. Выборка разделена на тренировочную и тестовую ( 150000 в тренировочной части, 101503 в тестовой).\n",
    "\n",
    "Данная решаемая задача будет ценна как кредиторам, так и заемщикам для оценки способности вернуть долг вовремя.\n",
    "\n",
    "### Описание признаков\n",
    "\n",
    "* 1 - __SeriousDlqin2yrs__ - будет ли просрочка более 90 дней в ближайшие 2 года (целевая метка) (Да/Нет)\n",
    "* 2 - __RevolvingUtilizationOfUnsecuredLines__ - общий баланс по кредитным картам и кредитным линиям, за исключением задолженностей по недвижимости задолженности по взносам, деленные на сумму кредитных лимитов (проценты)\n",
    "* 3 - __age__ - Возраст заемщика (в годах)\n",
    "* 4 - __NumberOfTime30-59DaysPastDueNotWorse__ - количество просрочек в 30-59 дней (Целое)\n",
    "* 5 - __DebtRatio__ - коэффициент задолженности, т.е. сумма ежемесячных платежей по долгам, алиментов и расходов на проживание, деленная на месячный доналоговый доход (проценты)\n",
    "* 6 - __Monthly Income__ - месячный доход (число с плавающей точкой)\n",
    "* 7 - __NumberOfOpenCreditLinesAndLoans__ - количество открытых кредитов и кредитных линий (Целое)\n",
    "* 8 - __NumberOfTimes90DaysLate__ - количество просрочек более 90 дней (Целое)\n",
    "* 9 - __NumberRealEstateLoansOrLines__ - количество ипотечных кредитов и кредитов на недвижимость, включая кредитные линии домашнего капитала (Целое)\n",
    "* 10 - __NumberOfTime60-89DaysPastDueNotWorse__ - количество просрочек в 60-89 дней за последние 2 года (Целое)\n",
    "* 11 - __NumberOfDependents__ - количество иждивенцев в семье (исключая самих заемщиков) (Целое)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def make_submission(predictions, fname):\n",
    "    out = pd.DataFrame(data=predictions, columns=['Probability'])\n",
    "    out.index += 1\n",
    "    out.to_csv(fname, index_label='id')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df = pd.read_csv('../../data/credit/cs-training.csv', index_col=0)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "feature_names = df.columns[1:]\n",
    "df.head()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(df.shape)\n",
    "df.describe(include = \"all\").T"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.info();"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.nunique()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Как видно из описания, категориальные признаки отсутствуют.\n",
    "\n",
    "Проверим на наличие NaN"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.isnull().sum()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "(df['MonthlyIncome'].isnull() & df['NumberOfDependents'].isnull()).sum()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "По всей видимости, часть заемщиков отказалась разглашать информацию о своих доходах а также о членах семьи. \n",
    "Для месячного дохода заменим NaN на среднее значение по выборке, для количества иждивенцев - на 0"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df['MonthlyIncome'].fillna(df['MonthlyIncome'].mean(), inplace=True)\n",
    "df['NumberOfDependents'].fillna(0, inplace=True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df['SeriousDlqin2yrs'].value_counts()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.isnull().sum()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Т.о., классы не равны по размеру, количество заемщиков, сильно просрочивших возврат кредита значительно меньше, чем вовремя вернувших его."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "__Визуализация данные__"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "sns.pairplot(df, hue='SeriousDlqin2yrs', size=4.5);"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "sns.set()\n",
    "fig, ax = plt.subplots(figsize=(12,10));\n",
    "sns.heatmap(df.corr('spearman'), cmap='PuOr', annot=True, ax=ax);"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### <center>Закономерности:</center>\n",
    "\n",
    "* Возраст заемщиков вне зависимости от значения целевой метки имеет распрделение, близкое к нормальному.\n",
    "* Количество открытых кредитов коррелирует с количеством кредитов на недвижимость, что можно объяснить общей активностью заемщика.\n",
    "* Целевая переменная зависит от количества предыдущих просрочек, чем больше по длительности была каждая просрочка, тем больше вероятность будущей. "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "__Предобработка данных__"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X_train = df.drop(columns=['SeriousDlqin2yrs'])\n",
    "y_train = df['SeriousDlqin2yrs']\n",
    "\n",
    "df_test = pd.read_csv('../../data/credit/cs-test.csv', index_col=0)\n",
    "df_test['MonthlyIncome'].fillna(df_test['MonthlyIncome'].mean(), inplace=True)\n",
    "df_test['NumberOfDependents'].fillna(0, inplace=True)\n",
    "\n",
    "X_test = df_test.drop(columns=['SeriousDlqin2yrs'])\n",
    "\n",
    "scaler = StandardScaler()\n",
    "X_train_scaled = scaler.fit_transform(X_train)\n",
    "X_test_scaled = scaler.transform(X_test)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "__Выбор метрики__"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "В качестве метрики используется ROC-AUC score, согласно соренованию. Данная метрика подходит для данной задачи, так как мы хотим определять тех заемщиков, которые сильно задержат выплату кредита, но при этом выдавать кредиты хорошим клиентам. Также эта метрика хорошо работает со случаем несбалансированных классов (как в нашем случае).\n",
    "\n",
    "Для валидации был выбран метод Stratified KFold (т.к. выборка не сбалансирована) на 5 фолдов."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.model_selection import cross_val_score"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Обучим 2 классификатора - логистическую регрессию и случайный лес.\n",
    "Сначала посмотрим на результаты при дефолтных параметрах."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "kfold = StratifiedKFold(n_splits=5, shuffle=True, random_state=RANDOM_STATE)\n",
    "logit = LogisticRegression(random_state=RANDOM_STATE, solver='lbfgs', verbose=0)\n",
    "forest = RandomForestClassifier(n_estimators=50, n_jobs=-1, random_state=RANDOM_STATE, verbose=0)\n",
    "\n",
    "scores = []\n",
    "\n",
    "classifiers = [logit, forest]\n",
    "for c in tqdm(classifiers, total=len(classifiers)):\n",
    "    scores.append(cross_val_score(c, X_train_scaled, y_train, scoring='roc_auc', cv=kfold,n_jobs=-1, verbose=1))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "np.mean(scores, axis=1)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "__Кросс-валидация и подбор параметров__\n",
    "\n",
    "Произведем подбор параметров. Для логистической регрессии подберем коэффициент регуляризции, для случайного леса -максимальную глубину и максимальное кол-во признаков."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%time\n",
    "logit_params = {'C': np.logspace(0, 2, 7)}\n",
    "forest_params = {'min_samples_leaf':[1, 3, 5, 7],\n",
    "                 'max_depth': [None] + list(range(5, 15))}\n",
    "\n",
    "logit_grid = GridSearchCV(logit, logit_params, scoring='roc_auc', n_jobs=-1,cv=kfold, verbose=1)\n",
    "forest_grid = GridSearchCV(forest, forest_params, scoring='roc_auc', n_jobs=-1,cv=kfold, verbose=1)\n",
    "\n",
    "scores_grid = []\n",
    "\n",
    "classifiers_grid = [logit_grid, forest_grid]\n",
    "for c in tqdm(classifiers_grid, total=len(classifiers_grid)):\n",
    "    c.fit(X_train_scaled, y_train)\n",
    "    scores_grid.append(c.best_score_)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(scores_grid)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Лучший результат дал случайный лес. Выведем подобранные параметры."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "forest_grid.best_params_"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "predictions = forest_grid.predict_proba(X_test_scaled)[:, 1]\n",
    "make_submission(predictions, 'sub1.csv')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Данная модель дает результат 0.860890 на public leaderboard и 0.864706 на private."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "__Создание новых признаков__"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Разделим заемщиков на 2 группы по возрасту (до и старше 18). Также создадим признак если ли иждевенцы у заемщика, количество возобновляемых кредитный линий (разность между общим кол-вом линий и заемов и количеством кредитов на недвижимость) и отношение кол-ва возобновляемых линий к кол-ву кредитов на недвижимость - данный признак показывает какой тип кредитов чаще берет заемщик."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df_features_train = pd.DataFrame(index=df.index)\n",
    "df_features_train['age<18'] = df['age'].apply(lambda x: 1 if x < 18 else 0)\n",
    "\n",
    "df_features_train['AgeDep'] = df['age'] / df['NumberOfDependents']\n",
    "df_features_train['AgeDep'][df['NumberOfDependents'] == 0] = 0\n",
    "\n",
    "df_features_train['NoDependents'] = (df['NumberOfDependents'] == 0)\n",
    "df_features_train['RevolvLines'] = df['NumberOfOpenCreditLinesAndLoans'].values - df['NumberRealEstateLoansOrLines'].values\n",
    "df_features_train['Rev2Estate'] = df_features_train['RevolvLines'] / (1 + df['NumberRealEstateLoansOrLines']) \n",
    "\n",
    "df_features_test = pd.DataFrame(index = df_test.index)\n",
    "df_features_test['age<18'] = df_test['age'].apply(lambda x: 1 if x < 18 else 0)\n",
    "\n",
    "df_features_test['AgeDep'] = df_test['age'] / df_test['NumberOfDependents']\n",
    "df_features_test['AgeDep'][df_test['NumberOfDependents'] == 0] = 0\n",
    "\n",
    "df_features_test['NoDependents'] = (df_test['NumberOfDependents'] == 0)\n",
    "df_features_test['RevolvLines'] = df_test['NumberOfOpenCreditLinesAndLoans'].values - df_test['NumberRealEstateLoansOrLines'].values\n",
    "df_features_test['Rev2Estate'] = df_features_test['RevolvLines'] / (1 + df_test['NumberRealEstateLoansOrLines']) "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X_train_feat = pd.concat([X_train, df_features_train], axis=1)\n",
    "X_test_feat = pd.concat([X_test, df_features_test], axis=1)\n",
    "\n",
    "scaler = StandardScaler()\n",
    "X_train_feat_scaled = scaler.fit_transform(X_train_feat)\n",
    "X_test_feat_scaled = scaler.transform(X_test_feat)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%time\n",
    "forest_best = forest_grid.best_estimator_\n",
    "result_feat = cross_val_score(forest_best, X_train_feat_scaled, y_train, cv=kfold, scoring='roc_auc',n_jobs=-1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(np.mean(result_feat))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "forest_best.fit(X_train_feat_scaled, y_train)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "predictions = forest_best.predict_proba(X_test_feat_scaled)[:, 1]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "make_submission(predictions, 'sub2.csv')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "После создания новых признаков улучшился результат на private:\n",
    "    0.860624 - на public;\n",
    "    0.866692 - на private."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "__Кривые валидации и обучения__"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def plot_with_std(x, data, **kwargs):\n",
    "        mu, std = data.mean(1), data.std(1)\n",
    "        lines = plt.plot(x, mu, '-', **kwargs)\n",
    "        plt.fill_between(x, mu - std, mu + std, edgecolor='none',\n",
    "                         facecolor=lines[0].get_color(), alpha=0.2)\n",
    "        \n",
    "def plot_learning_curve(clf, X, y, scoring, cv=5, random_state=17):\n",
    " \n",
    "    train_sizes = np.linspace(0.05, 1, 20)\n",
    "    n_train, val_train, val_test = learning_curve(clf,\n",
    "                                                  X, y, train_sizes=train_sizes, cv=cv,\n",
    "                                                  scoring=scoring, random_state=random_state, n_jobs=-1)\n",
    "    plot_with_std(n_train, val_train, label='training scores', c='green')\n",
    "    plot_with_std(n_train, val_test, label='validation scores', c='red')\n",
    "    plt.xlabel('Training Set Size'); plt.ylabel(scoring)\n",
    "    plt.legend()\n",
    "\n",
    "def plot_validation_curve(clf, X, y, cv_param_name, \n",
    "                          cv_param_values, scoring, cv=5, random_state=17):\n",
    "\n",
    "    val_train, val_test = validation_curve(clf, X, y, cv_param_name,\n",
    "                                           cv_param_values, cv=cv,\n",
    "                                                  scoring=scoring, n_jobs=-1)\n",
    "    plot_with_std(cv_param_values, val_train, \n",
    "                  label='training scores', c='green')\n",
    "    plot_with_std(cv_param_values, val_test, \n",
    "                  label='validation scores', c='red')\n",
    "    plt.xlabel(cv_param_name); plt.ylabel(scoring)\n",
    "    plt.legend()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plot_validation_curve(forest_best, X_train_feat_scaled, y_train, 'min_samples_leaf', [1, 3, 5, 7], scoring='roc_auc', cv=kfold, random_state=RANDOM_STATE);"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plot_learning_curve(forest_best, X_train_feat_scaled, y_train, scoring='roc_auc', cv=kfold)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "__Оценка модели с описанием выбранной метрики__\n",
    "\n",
    "Построена модель предсказания, предсказывающая просрочит ли заемщик выплату кредита на более чем 90 дней. В качестве целевой метрики была выбрана ROC-AUC score, позволяющая хорошо оценивать предсказания при сильно несбалансированной выборке. Построенная модель. Результат работы модели  $ROC-AUC \\approx 0.86$.\n",
    "\n",
    "Построены кривые обучения и валидационные кривые. Согласно ним, увеличение числа примеров больше 1000000 не дает существенного прироста в качестве."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "__Общие выводы__\n",
    "\n",
    "По результатам проведенного анализа, можно увидеть, что есть определенные зависимости и признаки просрочки кредита. \n",
    "Наблюдаются определенные закономерности - месячный доход, возраст заемщика.\n",
    "\n",
    "Имеет влияние количество и длительность предыдущих просрочек - с ростом их числа, заемщик становиться менее привлекательным в связи с высокой вероятностью новой просрочки.\n",
    "\n",
    "Также влияет и количество иждевенцев у заемщика - клиент с семьей реже не выплачивает кредит вовремя. \n",
    "\n",
    "В дальнейшем, можно добавить новые признаки (логарифмическая зависимость и т.д.)\n",
    "Также можно заменить случайный лес на градиентый бустинг."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.5.2"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
